################################################
## 1. Primitive Datatypes and Operators
################################################

################################################
## 2. Variables and Collections
################################################

# empty_dict = {}
# filled_dict = {"one": 1, "two": 2, "three": 3}

# print(filled_dict["one"])

# print(list(filled_dict.keys()))
# print(list(filled_dict.values()))

# print("one" in filled_dict)
# print(1 in filled_dict)

# print(filled_dict["four"])
# print(filled_dict.get("four"))

# print(filled_dict.get("one", "miss value"))

# filled_dict.setdefault("five", 5)
# filled_dict.setdefault("five", 6)

# filled_dict.update({"four": 4})
# filled_dict["four"] = 5

# del filled_dict["one"]

# print(filled_dict)

# print({'a': 1, **{'b': 2}})
# print({'a': 1, **{'a': 2}})

# Sets

# empty_set = set()
# some_set = {1, 2, 2, 3, 4, 4}
# print(empty_set)
# print(some_set)
# print(type(empty_set))

# elements of a set have to be immutable
# invalid_set = {[1], 1}
# valid_set = {(1,), 1}
# print(valid_set)

# filled_set = some_set
# filled_set.add(5)
# # print(filled_set)
# other_set = {3, 4, 5, 6}
# print(filled_set & other_set)
# print(filled_set | other_set)

# print({1, 2, 3, 4} - {2, 3, 5})
# print({2, 3, 5} - {1, 2, 3, 4})
# print({1, 2, 3, 4} ^ {2, 3, 5})
# print({2, 3, 5} ^ {1, 2, 3, 4})

# print({1, 2} >= {1, 2, 3})
# print({1, 2} <= {1, 2, 3})
# print(12 in {1, 2, 3})

# filled_set = {1, 2, 3}
# shallow_copy_set = filled_set
# shallow_copy_set.add(4)
# print(shallow_copy_set is filled_set)
# deep_copy = filled_set.copy()
# print(deep_copy is filled_set)

################################################
## 3. Control Flow and Iterables
################################################

# some_var = 5

# if some_var > 10:
#     print("some_var is totally bigger than 10.")
# elif some_var < 10:    # This elif clause is optional.
#     print("some_var is smaller than 10.")
# else:                  # This is optional too.
#     print("some_var is indeed 10.")

# """
# For loops iterate over lists
# prints:
#     dog is a mammal
#     cat is a mammal
#     mouse is a mammal
# """
# for animal in ["dog", "cat", "mouse"]:
#     # You can use format() to interpolate formatted strings
#     print("{} is a mammal.".format(animal))

# """
# "range(number)" returns an iterable of numbers
# from zero to the given number
# prints:
#     0
#     1
#     2
#     3
# """
# for i in range(4):
#     print(i)

# for i in range(4, 8):
#     print(i)

# print("=" * 8)

# for i in range(4, 8, 2):
#     print(i)

# """
# To loop over a list, and retrieve both the index and the value of each item in the list
# prints:
#     0 dog
#     1 cat
#     2 mouse
# """
# animals = ["dog", "cat", "mouse"]
# for index, value in enumerate(animals):
#     print(index, value)

# x = 0
# while x < 4:
#     print(x)
#     x += 1


# Handle exceptions with a try/except block
# try:
#     # Use "raise" to raise an error
#     raise IndexError("This is an index error")
# except IndexError as e:
#     pass # no-op
# except (TypeError, NameError):
#     pass
# else:
#     print("All good!") # Run only if the code in try raises no exceptions
# finally:
#     print("We can clean up resources here")

# try:
#     with open("myfile.txt") as f:
#         for line in f:
#             print(line)
# except Exception as e:
#     print(e)
# finally:
#     print("out")

# import json

# contents = {'aa': 12, "bb": 21}
# with open("myfile1.txt", "w+") as file:
#     file.write(str(contents))

# with open("myfile2.txt", "w+") as file:
#     file.write(json.dumps(contents))

# with open('myfile2.txt', "r+") as file:
#     contents = file.read()
# print(contents)

# with open('myfile2.txt', "r+") as file:
#     contents = json.load(file)
# print(contents)


################################################
## 4. Functions
################################################

# def add(x, y):
#     print("x is {} and y is {}".format(x, y))
#     return x + y
# print(add(5, 6))
# print(add(y = 8, x = 1))

# def varargs(*args):
#     print(type(args))
#     return args

# print(varargs(a = 1, 2, 3))

# def keyword_args(**kwargs):
#     return kwargs

# print(keyword_args(big="foot", loch="ness"))

# """
# all_the_args(1, 2, a = 3, b = 4) prints:
#     (1, 2)
#     {"a": 3, "b": 4}
# """
# def all_the_args(*args, **kwargs):
#     print(args)
#     print(kwargs)

# args = (1, 2, 3, 4)
# kwargs = {"a": 3, "b": 4}

# # all_the_args(1, 2, a = 3, b = 4)
# # all_the_args(*args)
# # all_the_args(**kwargs)
# all_the_args(*args, **kwargs)

# Returning multiple values (with tuple assignments)
# def swap(x, y):
#     return y, x # Return multiple values as a tuple without the parenthesis.
#                 # (Note: parenthesis have been excluded but can be included)

# x = 1
# y = 3
# x, y = swap(x, y)
# print(x)
# print(y)

# Function Scope
# x = 5

# def set_x(num):
#     x = num
#     print("set_x:", x)

# def set_global_x(num):
#     global x
#     print("global:", x)
#     x = num
#     print("global2:", x)

# def print_x():
#     print("print:", x)

# print("start:", x)

# print_x()
# set_x(43)
# set_global_x(6)
# print("end:", x)

# Python has first class functions
# def create_adder(x):
#     def adder(y):
#         return x + y
#     return adder

# add_10 = create_adder(10)
# print(add_10(3))

# There are also anonymous functions
# print((lambda x: x > 2)(3))
# print((lambda x, y: x ** 2 + y ** 2)(2, 1))

# There are built-in higher order functions
# print(list(map(add_10, [1, 2, 3])))
# print(list(map(max, [1, 2, 3], [4, 2, 1])))

# print(list(filter(lambda x: x > 5, [3, 4, 5, 6, 7])))

# print([add_10(i) for i in [1, 2, 3]])
# print([x for x in [3, 4, 5, 6, 7] if x > 5])

# print({x for x in 'abcddeef' if x not in 'abc'})
# print({str(x) + '-': x ** 2 for x in range(5)})

################################################
## 5. Modules
################################################

# You can import modules
# import math
# print(math.sqrt(16))

# You can get specific functions from a module
# from math import ceil, floor
# print(type(ceil(3.7)))
# print(type(floor(3.7)))

# You can import all functions from a module.
# Warning: this is not recommended
# from math import *

# You can shorten module names
# import math as m
# print(math.sqrt(16) == m.sqrt(16))

# import math
# print(dir(math))

################################################
## 6. Classes
################################################

# We use the "class" statement to create a class
# class Human:
    
#     # A class attribute. It is shared by all instances of this class
#     species = "H. sapiens"

#     def __init__(self, name):
#         self.name = name
#         self._age = 0
    
#     def say(self, msg):
#         print("{name}: {message}".format(name=self.name, message=msg))
    
#     def sing(self):
#         return 'yo... microphone check... one two... one two...'
    
#     @classmethod
#     def get_species(cls):
#         return cls.species
    
#     @staticmethod
#     def grunt():
#         return "*grunt*"
    
#     @property
#     def age(self):
#         return self._age

#     @age.setter
#     def age(self, age):
#         self._age = age
    
#     @age.deleter
#     def age(self):
#         del self._age

# if __name__=='__main__':
#     i = Human(name="Ian")
#     i.say("hi")
#     j = Human("Joel")
#     j.say("hello")

#     i.say(i.get_species())
#     Human.species = "H. neanderthalensis"
#     i.say(i.get_species())
#     j.say(j.get_species())
    
#     print(Human.grunt())

#     print(i.grunt())

#     i.age = 42
#     i.say(i.age)
#     j.say(j.age)
#     del i.age


# from human import Human

# class Superhero(Human):

#     species = "Superhuman"
    
#     def __init__(self, name, movie=False, superpowers=["super strength", "bulletproofing"]):
#         self.fictional = True
#         self.movie = movie
#         self.superpowers = superpowers

#         super().__init__(name)
    
#     def sing(self):
#         return "Dun, dun, DUN!"
    
#     def boast(self):
#         for power in self.superpowers:
#             print("I wield the power of {pow}!".format(pow=power))

# if __name__=='__main__':
#     sup = Superhero(name="Tick")

#     if isinstance(sup, Human):
#         print("I am human")
#     if type(sup) is Superhero:
#         print("I am superhero")
    
#     print(Superhero.__mro__)

#     print(sup.get_species())

#     print(sup.sing())

#     sup.say('Spoon')

#     sup.boast()

#     sup.age = 31
#     print(sup.age)

#     print('Am I Oscar eligible?' + str(sup.movie))

# from bat import Bat

# class Batman(Superhero, Bat):

#     def __init__(self, *args, **kwargs):
#         Superhero.__init__(self, 'anonymous', movie=True, superpowers=['Wealthy'], *args, **kwargs)
#         Bat.__init__(self, *args, can_fly=False, **kwargs)
#         self.name = 'Sad Affleck'
    
#     def sing(self):
#         return 'nan nan nan nan nan batman!'

# if __name__=='__main__':
#     sup = Batman()

#     print(Batman.__mro__)

#     print(sup.get_species())
#     print(sup.sing())
#     sup.say('I agree')
#     print(sup.sonar)
#     sup.age = 100
#     print(sup.age)
#     print('Can I fly? ' + str(sup.fly))

################################################
## 7. Advanced
################################################

# Generators help you make lazy code.
# def double_numbers(iterable):
#     for i in iterable:
#         yield i + i

# for i in double_numbers(range(1, 900000000)):
#     print(i)
#     if i >= 30:
#         break

# values = (-x for x in [1, 2, 3, 4, 5])
# for x in values:
#     print(x)

# values = (-x for x in [1, 2, 3, 4, 5])
# gen_to_list = list(values)
# print(gen_to_list)

# # Decorators
# # In this example `beg` wraps `say`. If say_please is True then it will change the returned message
# from functools import wraps

# def beg(target_function):
#     @wraps(target_function)
#     def wrapper(*args, **kwargs):
#         msg, say_please = target_function(*args, **kwargs)
#         if say_please:
#             return "{} {}".format(msg, "Please! I am poor :(")
#         return msg
    
#     return wrapper

# @beg
# def say(say_please=False):
#     msg = "Can you buy me a beer?"
#     return msg, say_please

# print(say())
# print(say(say_please=True))